#include "main.h"

static volatile SDL_Thread *thread = NULL;
typedef struct {
    engine_module_t *module;
    volatile int op;
    uint32_t finished;
    char fn[PATH_MAX + FILENAME_MAX + 1];
    int isdir;
} worker_t;
static worker_t worker;

char engine_game[256];
engine_module_t engine_modules[128];
int nummod = 0, engine_features = 0, engine_backup = 1, engine_convert = 1, engine_keylen = 0, lowertetrad = 0;
uint8_t engine_key[256] = { 0 };
uint64_t engine_ram = 0;

/**
 * Register an engine module (called by ENGINE_MODULE() macro)
 */
int modsrt(const void *a, const void *b) { return strcmp(((engine_module_t*)a)->mod, ((engine_module_t*)b)->mod); }
void engine_module(char *name, char *mod,
    engine_detect_t detect,
    engine_load_t load,
    engine_unload_t unload,
    engine_decrypt_t decrypt,
    engine_drawmap_t drawmap,
    engine_translate_t translate
) {
    engine_modules[nummod].name = name;
    engine_modules[nummod].mod = mod;
    engine_modules[nummod].detect = detect;
    engine_modules[nummod].load = load;
    engine_modules[nummod].unload = unload;
    engine_modules[nummod].decrypt = decrypt;
    engine_modules[nummod].drawmap = drawmap;
    engine_modules[nummod].translate = translate;
    nummod++;
    qsort(engine_modules, nummod, sizeof(engine_module_t), modsrt);
}

/**
 * Worker thread's main loop
 * receive command and execute it
 */
int engine_worker(void *data)
{
    uint64_t ram;
    uint32_t start, diff;

    (void)data;
    debug("worker starting");
    while(worker.op != EF_STOP) {
        for(worker.op = EF_NOOP; worker.op == EF_NOOP;) SDL_Delay(100);
        if(worker.module) {
            start = SDL_GetTicks();
            debug("worker got command %d (time %u)", worker.op, start);
            engine_ram = 0;
            switch(worker.op) {
                case EF_LOAD:
                    debug("worker load '%s' isdir %d", worker.fn, worker.isdir);
                    (*worker.module->load)(worker.fn, worker.isdir);
                    if(verbose) {
                        ram = (engine_ram + 1023) >> 10; diff = 'K';
                        if(ram > 1023) { ram = (ram + 1023) >> 10; diff = 'M';
                            if(ram > 1023) { ram = (ram + 1023) >> 10; diff = 'G'; } }
                        debug("worker game '%s' loaded, RAM %u%ciB allocated.", engine_game, (uint32_t)ram, diff);
                    }
                    if(numsheets && !numsprites) {
                        debug("worker scan sprites numsheet %d", numsheets);
                        sprites_scansheets();
                    }
                    loaded = 1;
                    tab = TAB_INFO;
                break;
                case EF_DECRYPT:
                    debug("worker decrypt '%s' isdir %d backup %d convert %d", worker.fn, worker.isdir, engine_backup, engine_convert);
                    (*worker.module->decrypt)(worker.fn, worker.isdir);
                    loaded = 1;
                break;
                case EF_TEXTS:
                    debug("worker translate '%s' numcol %d numrow %d curcol %d '%s'", worker.fn,
                        numcol, numrow, curcol, texts && texts[0].value[curcol] ? texts[0].value[curcol] : "");
                    (*worker.module->translate)(worker.fn);
                break;
                case EF_SCANSPR:
                    debug("worker scan sprites numsheets %d", numsheets);
                    sprites_scansheets();
                break;
            }
            diff = SDL_GetTicks() - start;
            debug("worker done (time %u, elapsed %u).", diff + start, diff);
            if(worker.op >= EF_LOAD) worker.finished = diff;
        }
        status(0, 0, 0);
    }
    debug("worker stopped");
    return 0;
}

/**
 * Wrapper to call engine module's draw map
 */
void engine_map(map_t *map)
{
    if(map && worker.module)
        (*worker.module->drawmap)(map);
}

/**
 * Wrapper to call engine module's save translation function
 */
void engine_translate(void)
{
    if(texts && numcol > 0 && numrow > 1 && worker.module)
        worker.op = EF_TEXTS;
}

/**
 * Free engine related memory
 */
void engine_free(void)
{
    sprites_free();
    models_free();
    maps_free();
    audio_free();
    texts_free();
    if(worker.module) { (*worker.module->unload)(); worker.module = NULL; }
    memset(&worker, 0, sizeof(worker));
    memset(engine_game, 0, sizeof(engine_game));
    engine_features = engine_backup = 0;
}

/**
 * Stop the worker thread
 */
void engine_stop(void)
{
    int ret, timeout = 30;

    if(thread) {
        while(--timeout > 0 && worker.op != EF_NOOP) SDL_Delay(250);
        worker.op = EF_STOP;
        if(timeout > 0) SDL_WaitThread((SDL_Thread*)thread, &ret);
        thread = NULL;
    }
    engine_free();
}

/**
 * Detect game type and start worker
 */
void engine_init(char *fn)
{
    uint8_t tmp[4096], *buf;
    uint64_t size = 0;
    void *f;
    int i, timeout = 30, retry = 4;
    char *fn2, *name;

    if(!fn || !*fn) return;
    engine_free();
    if(!thread) {
        do {
            worker.op = -2;
            thread = SDL_CreateThread(engine_worker, "engine", NULL);
            for(timeout = 30; timeout > 0 && worker.op != EF_NOOP; timeout--) SDL_Delay(250);
            if(!timeout) { worker.op = EF_STOP; SDL_Delay(250); thread = NULL; }
        } while(worker.op != EF_NOOP && --retry);
        if(!retry) { debug("unable to start worker, should never happen!"); return; }
        debug("worker synced");
    }
    strcpy(worker.fn, fn);
    i = strlen(worker.fn);
    if(i && (worker.fn[i - 1] == '/' || worker.fn[i - 1] == '\\')) worker.fn[--i] = 0;
    if((f = fileopen(fn, &size))) {
        worker.isdir = 0;
        fileread(f, tmp, sizeof(tmp));
    } else {
        worker.isdir = 1;
    }
    debug("detect format '%s'", worker.fn);
    for(i = 0; i < nummod; i++)
        if((*engine_modules[i].detect)(fn, worker.isdir ? NULL : tmp, size)) {
            debug("engine '%s'", engine_modules[i].mod);
            tab = TAB_INFO;
            worker.module = &engine_modules[i];
            worker.op = decrypt ? EF_DECRYPT : EF_LOAD;
            return;
        }
    /* fallback, let's see if it's an unencrypted asset */
    if(!worker.isdir && ((sprites_detect(tmp) || audio_detect(tmp)) && (buf = (uint8_t*)getfile(fn, &size)))) {
        fn2 = fn;
        if(fn2[0] == '/' || fn2[0] == '\\') fn2++;
        if((name = strrchr(fn2, '/'))) name++; else
        if((name = strrchr(fn2, '\\'))) name++;
        else name = fn2;
        debug("engine 'asset'");
        worker.module = NULL;
        strncpy(engine_game, name, 255);
        if(!(fileasset(-1, name, buf, size))) free(buf);
        tab = numsprites ? TAB_SPRITES : (numbgm ? TAB_BGM : TAB_SFX);
        refresh = loaded = 1;
        return;
    }
    debug("unknown format");
    msgbox(1, UNSUPPORTED, fn);
}

/**
 * Helper to display RAM usage
 */
void engine_view_ram(void)
{
    uint64_t ram;
    int i;
    char tmp[64];

    if(engine_ram > 1023 && worker.op == EF_LOAD) {
        ram = (engine_ram + 1023) >> 10; i = 'K';
            if(ram > 1023) { ram = (ram + 1023) >> 10; i = 'M';
                if(ram > 1023) { ram = (ram + 1023) >> 10; i = 'G'; } }
        sprintf(tmp, "(RAM %4u%ciB)", (uint32_t)ram, i);
        text((screen_w - textlen(tmp)) / 2, (screen_h - 128) / 2 + 18, BG + 0x181818, tmp);
    }
}

/**
 * Helper to get a digit from user keypress
 */
static void keydigit(uint8_t v)
{
    if(v < 16) {
        if(!lowertetrad) {
            engine_key[engine_keylen] = v << 4;
            lowertetrad = 1;
        } else if(engine_keylen < (int)sizeof(engine_key)) {
            engine_key[engine_keylen++] |= v;
            lowertetrad = 0;
        }
    }
}

/**
 * UI Controller - key input page
 */
void engine_keyinput_ctrl(void)
{
    char *tmp, *str;

    switch(event.type) {
        case SDL_KEYDOWN:
            switch(event.key.keysym.sym) {
                case SDLK_SPACE: break;
                case SDLK_BACKQUOTE: case SDLK_KP_0: keydigit(0); break;
                case SDLK_KP_1: case SDLK_KP_2: case SDLK_KP_3: case SDLK_KP_4: case SDLK_KP_5:
                case SDLK_KP_6: case SDLK_KP_7: case SDLK_KP_8: case SDLK_KP_9:
                    keydigit(event.key.keysym.sym - SDLK_KP_1 + 1);
                break;
                case SDLK_0: case SDLK_1: case SDLK_2: case SDLK_3: case SDLK_4: case SDLK_5: case SDLK_6: case SDLK_7: case SDLK_8: case SDLK_9:
                    keydigit(event.key.keysym.sym - SDLK_0);
                break;
                case SDLK_a: case SDLK_b: case SDLK_c: case SDLK_d: case SDLK_e: case SDLK_f:
                    keydigit(event.key.keysym.sym - SDLK_a + 10);
                break;
                case SDLK_BACKSPACE:
                    if(lowertetrad) { engine_key[engine_keylen] &= 0xF0; lowertetrad = 0; }
                    else if(engine_keylen > 0) { engine_key[engine_keylen--] = 0; lowertetrad = 1; }
                break;
                case SDLK_v:
                    /* handle copy'n'paste */
                    if((tmp = str = SDL_GetClipboardText())) {
                        for(engine_keylen = lowertetrad = 0; engine_keylen < (int)sizeof(engine_key) && *str; str++)
                            keydigit(*str >= '0' && *str <= '9' ? *str - '0' : (*str >= 'a' && *str <= 'f' ? *str - 'a' + 10 :
                                (*str >= 'A' && *str <= 'F' ? *str - 'A' + 10 : 255)));
                        SDL_free(tmp);
                    }
                break;
            }
        break;
    }
}

/**
 * UI View - key input page
 */
void engine_keyinput_view(void)
{
    uint32_t light = BG + 0x282828;
    char tmp[16];
    int i, x;

    crop_w = screen_w - 16;
    text(16 , 16, FG, lang[ENCKEY]);
    txtfld(16, 36, screen_w - 32, 20, "");
    x = (screen_w - 36) / 18 - 1;
    for(i = (engine_keylen > x ? engine_keylen - x : 0), x = 18; i < engine_keylen; i++) {
        sprintf(tmp, "%02X", engine_key[i]);
        x = text(x, 38, light, tmp) + 2;
    }
    if(engine_keylen < (int)sizeof(engine_key)) {
        if(!lowertetrad) { x = text(x, 38, FG, "0"); x = text(x, 38, light, "0"); }
        else { sprintf(tmp, "%X", engine_key[engine_keylen] >> 4); x = text(x, 38, light, tmp); x = text(x, 38, FG, "0"); }
    }
    sprintf(tmp, "%u", engine_keylen);
    x = text(16, 60, BG + 0x202020, "([0]-[9], [A]-[F], [Ctrl]+[V])") + 8;
    x = text(x, 60, BG + 0x202020, lang[ENCLEN]) + 8;
    text(x, 60, FG, tmp);
}

/**
 * UI Controller - about page
 */
void engine_info_ctrl(void)
{
    if(worker.finished) {
        refresh = 1;
        worker.finished = 0;
    }
    switch(event.type) {
        case SDL_MOUSEBUTTONDOWN:
            if(engine_features & EF_BACKUP && field == 0) engine_backup ^= 1;
            if(engine_features & EF_CONVERT && field == 1) engine_convert ^= 1;
        break;
        case SDL_MOUSEBUTTONUP:
            if((engine_features & EF_DECRYPT) && field == 2 && pressedfield == 2) worker.op = EF_DECRYPT;
            if(numsheets && field == 3 && pressedfield == 3) worker.op = EF_SCANSPR;
        break;
        case SDL_KEYDOWN:
            switch(event.key.keysym.sym) {
                case SDLK_SPACE:
                    if(engine_features & EF_BACKUP) engine_backup ^= 1;
                    if(engine_features & EF_CONVERT) engine_convert ^= 1;
                break;
                case SDLK_RETURN: field = pressedfield = 2; break;
            }
        break;
        case SDL_KEYUP:
            switch(event.key.keysym.sym) {
                case SDLK_RETURN: if(engine_features & EF_DECRYPT) worker.op = EF_DECRYPT; break;
            }
        break;
    }
}

/**
 * UI View - about page
 */
void engine_info_view(void)
{
    char tmp[256];
    int i, j, w = 0;

    crop_w = screen_w - 16;
    for(i = SPRITES; i <= FORMAT; i++) {
        j = textlen(lang[i]);
        if(j > w) w = j;
    }
    j = 16;
    text(16 + w - textlen(lang[TITLE]), j, FG, lang[TITLE]);
    txtfld(24 + w, j, screen_w - w - 40, 16, engine_game);

    j += 20;
    text(16 + w - textlen(lang[FORMAT]), j, FG, lang[FORMAT]);
    txtfld(24 + w, j, screen_w - w - 40, 16, worker.module ? worker.module->name : "asset");

    if(numsheets) {
        j += 20; sprintf(tmp, "%8u", numsheets);
        text(16 + w - textlen(lang[SHEETS]), j, FG, lang[SHEETS]);
        txtfld(24 + w, j, 64, 16, tmp);
    }
    if(numsprites) {
        j += 20; sprintf(tmp, "%8u", numsprites);
        text(16 + w - textlen(lang[SPRITES]), j, FG, lang[SPRITES]);
        txtfld(24 + w, j, 64, 16, tmp);
    }
    if(nummodels) {
        j += 20; sprintf(tmp, "%8u", nummodels);
        text(16 + w - textlen(lang[MODELS]), j, FG, lang[MODELS]);
        txtfld(24 + w, j, 64, 16, tmp);
    }
    if(nummaps) {
        j += 20; sprintf(tmp, "%8u", nummaps);
        text(16 + w - textlen(lang[MAPS]), j, FG, lang[MAPS]);
        txtfld(24 + w, j, 64, 16, tmp);
    }
    if(numsfx) {
        j += 20; sprintf(tmp, "%8u", numsfx);
        text(16 + w - textlen(lang[SFX]), j, FG, lang[SFX]);
        txtfld(24 + w, j, 64, 16, tmp);
    }
    if(numbgm) {
        j += 20; sprintf(tmp, "%8u", numbgm);
        text(16 + w - textlen(lang[BGM]), j, FG, lang[BGM]);
        txtfld(24 + w, j, 64, 16, tmp);
    }
    if(numvideos) {
        j += 20; sprintf(tmp, "%8u", numvideos);
        text(16 + w - textlen(lang[VIDEOS]), j, FG, lang[VIDEOS]);
        txtfld(24 + w, j, 64, 16, tmp);
    }
    if(numrow > 1) {
        j += 20; sprintf(tmp, "%8u", numrow - 1);
        text(16 + w - textlen(lang[TEXTS]), j, FG, lang[TEXTS]);
        txtfld(24 + w, j, 64, 16, tmp);
        if(numcol > 1) {
            sprintf(tmp, "(%u %s)", numcol, lang[LANGS]);
            text(16 + 64 + 12 + w, j, BG + 0x282828, tmp);
        }
    }

    if(worker.module) {
        i = (screen_w - 12) / 3;
        if(engine_features & EF_DECRYPT) {
            j = screen_h - 42;
            if(engine_features & EF_BACKUP) {
                fields[0].x = 16; fields[0].y = j; fields[0].w = screen_w - 32; fields[0].h = 16;
                chkbox(fields[0].x, fields[0].y, engine_backup, lang[BACKUP]);
                j -= 20;
            }
            if(engine_features & EF_CONVERT) {
                fields[1].x = 16; fields[1].y = j; fields[1].w = screen_w - 32; fields[1].h = 16;
                chkbox(fields[1].x, fields[1].y, engine_convert, lang[CONVERT]);
            }
            fields[2].x = 2; fields[2].y = screen_h - 23; fields[2].w = i; fields[2].h = 21;
            button(fields[2].x, fields[2].y, i - 4, 21, pressedfield == 2, lang[DECRYPT]);
        }
        if(numsheets) {
            fields[3].x = 2 + i; fields[3].y = screen_h - 23; fields[3].w = i; fields[3].h = 21;
            button(fields[3].x, fields[3].y, i - 4, 21, pressedfield == 3, lang[SCANSPR]);
        }
        /* shit, it is sometimes so fast (less than 1/10 sec) that users don't even notice, so we need a "Finished" message */
        crop_w = screen_w - 2;
        box(2 + 2 * i, screen_h - 23, screen_w - 4 - 2 * i, 21, BG - 0x080808);
        if(worker.finished) {
            j = text(4 + 2 * i, screen_h - 21, FG, lang[FINISHED]);
            if(worker.finished < 1000) sprintf(tmp, " (%ums)", worker.finished); else
            if(worker.finished < 60000) sprintf(tmp, " (%us)", (worker.finished + 999) / 1000);
            else sprintf(tmp, " (%um)", (worker.finished + 59999) / 60000);
            text(j, screen_h - 22, FG, tmp);
        }
    }
}
